from .BaseParser import BaseParser
from loguru import logger
from bs4 import BeautifulSoup
import json
import re
from datetime import datetime
from typing import List, Dict, Optional, Any
import time

class PerformanceForecastParser(BaseParser):
    def __init__(self):
        super().__init__()
        self.base_url = "https://data.eastmoney.com/bbsj/yjyg.html"
        self.category = "业绩预告"
    
    def get_news_list(self, tag="", page=1, page_size=20, **kwargs):
        """获取业绩预告新闻列表"""
        try:
            logger.info(f"获取业绩预告新闻: tag={tag}, page={page}")
            
            # 直接通过API获取业绩预告数据
            api_data = self._fetch_performance_forecast_api()
            if api_data:
                logger.info(f"通过API获取到 {len(api_data)} 条业绩预告数据")
                return api_data[:20]
            
            # 如果API获取失败，尝试访问业绩预告页面
            response = self.session.get(self.base_url, timeout=15)
            if response.status_code == 200:
                return self._parse_page(response.text, tag)
            
            return []
        except Exception as e:
            logger.error(f"获取业绩预告新闻失败: {str(e)}")
            return []
    
    def _get_json_patterns(self) -> List[str]:
        """获取业绩预告数据的JSON提取模式"""
        return [
            r'var\s+(?:data|reportData|forecastData)\s*=\s*(\[.*?\]);',
            r'"(?:data|list|forecastList)"\s*:\s*(\[.*?\])',
            r'yjygData\s*[=:]\s*(\[.*?\])',
        ]
    
    def _get_table_selectors(self) -> List[str]:
        """获取业绩预告数据表格选择器"""
        return [
                'table tbody tr',
                '.dataview tbody tr',
                '.table-responsive tbody tr',
                '[data-code]',
                '#table_wrapper tbody tr',
            ]
            
    def _is_relevant_title(self, title_text: str) -> bool:
        """判断标题是否与业绩预告相关"""
        keywords = ['业绩预告', '公告', '预告', '业绩', '预测']
        return any(keyword in title_text for keyword in keywords)
    
    def _extract_from_element(self, element) -> Dict:
        """从业绩预告表格元素中提取信息"""
        try:
            cells = element.find_all(['td', 'th'])
            if not cells or len(cells) < 2:
                return None
            
            cell_texts = [cell.get_text(strip=True) for cell in cells if cell.get_text(strip=True)]
            if len(cell_texts) < 2:
                return None
            
            stock_name = cell_texts[0]
            stock_code = cell_texts[1] if len(cell_texts) > 1 else ''
            forecast_date = cell_texts[2] if len(cell_texts) > 2 else ''
            
            # 检查股票代码格式
            if not re.match(r'\d{6}', stock_code) and len(cell_texts) >= 3:
                for i, text in enumerate(cell_texts):
                    if re.match(r'\d{6}', text):
                        stock_code = text
                        if i > 0:
                            stock_name = cell_texts[i-1]
                            break
            
            if not stock_name or len(stock_name) < 2:
                return None
            
            # 尝试从页面中提取公告标题
            title = self._extract_title_from_element(element)
            if not title:
                title = f"{stock_code} {stock_name} - 业绩预告公告" if stock_name and stock_code else "业绩预告公告"
            
            forecast_data = {
                'SECURITY_NAME_ABBR': stock_name,
                'SECURITY_CODE': stock_code,
                'NOTICE_DATE': forecast_date,
                'PREDICT_TYPE': '业绩预告',
                'TITLE': title
            }
            
            return self._convert_data_to_news(forecast_data)
        except:
            return None
    
    def _extract_title_from_element(self, element) -> str:
        """从元素中提取标题"""
        # 查找可能的标题元素
        title_elements = element.find_all(['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'strong', 'b'])
        for title_elem in title_elements:
            title_text = title_elem.get_text(strip=True)
            if title_text and len(title_text) > 5 and ('业绩预告' in title_text or '公告' in title_text or '预告' in title_text):
                return title_text
        
        # 如果没有找到标题，尝试从链接文本中提取
        link_elements = element.find_all('a')
        for link in link_elements:
            link_text = link.get_text(strip=True)
            if link_text and len(link_text) > 5 and ('业绩预告' in link_text or '公告' in link_text):
                return link_text
        
        return ''
    
    def _convert_data_to_news(self, forecast_item: Dict) -> Dict:
        """将业绩预告数据转换为新闻格式，丰富指标字段并用“业绩变动”作为标题内容"""
        try:
            # 基本字段
            stock_name = forecast_item.get('SECURITY_NAME_ABBR', forecast_item.get('SNAME', ''))
            stock_code = forecast_item.get('SECURITY_CODE', forecast_item.get('SCODE', ''))
            notice_date = forecast_item.get('NOTICE_DATE', forecast_item.get('NOTICEDATE', ''))

            if not stock_name:
                return None

            # 业绩变动（用于标题的“公告内容”）
            change_text_candidates = [
                forecast_item.get('PREDICT_TYPE_DESC'),
                forecast_item.get('PREDICT_RESULT'),
                forecast_item.get('RESULT_TYPE'),
                forecast_item.get('CHANGE_DESC'),
                forecast_item.get('PREDICT_TYPE'),
            ]
            change_text = next((x for x in change_text_candidates if isinstance(x, str) and x.strip()), '业绩预告')

            # 其他指标字段提取
            indicator = (
                forecast_item.get('PREDICT_FINANCE')
                or forecast_item.get('PREDICT_INDICATOR')
                or forecast_item.get('INDEX_NAME')
                or '净利润'
            )

            # 预测数值（区间合并）
            # 数值单位处理：API返回为元，转换为万元显示
            def to_wanyuan(v):
                try:
                    return round(float(v) / 10000, 2)
                except Exception:
                    return v

            val_min = (
                forecast_item.get('PREDICT_AMT_LOWER')
                or forecast_item.get('PREDICT_AMT_MIN')
                or forecast_item.get('MIN_VALUE')
                or forecast_item.get('PREDICT_MIN')
            )
            val_max = (
                forecast_item.get('PREDICT_AMT_UPPER')
                or forecast_item.get('PREDICT_AMT_MAX')
                or forecast_item.get('MAX_VALUE')
                or forecast_item.get('PREDICT_MAX')
            )
            value_single = (
                forecast_item.get('FORECAST_JZ')
                or forecast_item.get('PREDICT_AMT')
                or forecast_item.get('PREDICT_VALUE')
            )
            predict_value = None
            if value_single is not None:
                predict_value = f"{to_wanyuan(value_single)} 万元"
            elif val_min is not None and val_max is not None:
                predict_value = f"{to_wanyuan(val_min)} ~ {to_wanyuan(val_max)} 万元"

            # 同比/环比
            # 同比(ADD_AMP_LOWER/UPPER 为百分比数值)，优先展示区间
            yoy_low = forecast_item.get('ADD_AMP_LOWER') or forecast_item.get('PREDICT_RATIO_LOWER')
            yoy_up = forecast_item.get('ADD_AMP_UPPER') or forecast_item.get('PREDICT_RATIO_UPPER')
            yoy = None
            if yoy_low is not None and yoy_up is not None:
                yoy = f"{yoy_low}% ~ {yoy_up}%"
            elif yoy_low is not None:
                yoy = f"{yoy_low}%"
            # 环比均值（PREDICT_HBMEAN 为小数，转%）
            qoq_mean = forecast_item.get('PREDICT_HBMEAN')
            qoq = f"{round(qoq_mean * 100, 2)}%" if isinstance(qoq_mean, (int, float)) else None

            # 变动原因
            reason = (
                forecast_item.get('CHANGE_REASON_EXPLAIN')
                or forecast_item.get('CHANGE_REASON')
                or forecast_item.get('EXPLAIN_TEXT')
                or forecast_item.get('REMARK')
            )

            # 预告类型
            forecast_type = forecast_item.get('PREDICT_TYPE') or forecast_item.get('TYPE') or '业绩预告'

            # 上年同期值
            last_year_value = forecast_item.get('PREYEAR_SAME_PERIOD') or forecast_item.get('SAME_PERIOD') or forecast_item.get('LAST_YEAR_VALUE')
            if last_year_value is not None:
                last_year_value = f"{to_wanyuan(last_year_value)} 万元"

            # 构造标题：证券代码 证券名称 - 业绩变动
            title = f"{stock_code} {stock_name} - {change_text}" if stock_code else f"{stock_name} - {change_text}"

            # 摘要与内容
            summary = f"{stock_name}{change_text}；指标：{indicator}；"
            content_lines = []
            if indicator:
                content_lines.append(f"预测指标：{indicator}")
            if predict_value:
                content_lines.append(f"预测数值：{predict_value}")
            if yoy is not None:
                content_lines.append(f"业绩变化(同比)：{yoy}")
            if qoq is not None:
                content_lines.append(f"业绩变化(环比)：{qoq}")
            if reason:
                content_lines.append(f"业绩变动原因：{reason}")
            if forecast_type:
                content_lines.append(f"预告类型：{forecast_type}")
            if last_year_value is not None:
                content_lines.append(f"上年同期值：{last_year_value}")

            content = "\n".join(content_lines)

            # 提取公告ID（如果存在）
            notice_id = forecast_item.get('ARTICLEID') or forecast_item.get('notice_id') or ''
            
            # URL - 修复URL格式，使用正确的东方财富新闻链接格式
            if notice_id and stock_code:
                # 使用正确的东方财富新闻链接格式：https://data.eastmoney.com/notices/detail/股票代码/公告ID.html
                url = f"https://data.eastmoney.com/notices/detail/{stock_code}/{notice_id}.html"
            elif stock_code:
                # 如果只有股票代码，使用股票公告页面
                url = f"https://data.eastmoney.com/notices/stock/{stock_code}.html"
            else:
                # 默认页面
                url = "https://data.eastmoney.com/notices/hsa/6.html"

            # 关键词（去掉“公告”类词）
            kw = ['业绩预告', stock_code, stock_name, forecast_type]
            kw = [str(x) for x in kw if x and str(x) not in ['公告', '公示', '公告信息']]

            return {
                'title': title,
                'url': url,
                'source': '东方财富',
                'publish_time': self._normalize_time(notice_date),
                'summary': summary,
                'content': content,
                'category': self.category,
                'keywords': kw,
                'crawl_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
                'stock_codes': json.dumps([stock_code]) if stock_code else None,
                'stock_name': stock_name,
                # 结构化扩展字段（用于JSON落地和前端展示）
                'forecast_indicator': indicator,
                'forecast_value': predict_value,
                'yoy_change': yoy,
                'qoq_change': qoq,
                'change_reason': reason,
                'forecast_type': forecast_type,
                'last_year_value': last_year_value
            }
        except Exception as e:
            logger.debug(f"转换业绩预告数据失败: {str(e)}")
            return None
    

    
    def _fetch_performance_forecast_api(self) -> List[Dict]:
        """
        通过API获取业绩预告数据
        
        Returns:
            业绩预告数据列表
        """
        try:
            # 东方财富业绩预告API接口
            api_url = "https://datacenter-web.eastmoney.com/api/data/v1/get"
            
            # 优化API参数，获取更准确和多样化的数据
            params = {
                'sortColumns': 'NOTICE_DATE,SECURITY_CODE',
                'sortTypes': '-1,-1',
                'pageSize': '50',
                'pageNumber': '1',
                'reportName': 'RPT_PUBLIC_OP_NEWPREDICT',
                'columns': 'ALL',
                # 添加过滤条件，获取最近的业绩预告数据
                'filter': '(REPORT_DATE=\'2025-06-30\')'
            }
            
            try:
                logger.info(f"尝试API接口: {api_url}")
                # 使用统一请求方法（包含User-Agent/Referer/Cookie、重试、JSONP兼容）
                referer = 'https://data.eastmoney.com/bbsj/yjyg.html'
                data = self._request_json(api_url, params=params, referer=referer, timeout=15, max_retries=3)
                
                if data is not None:
                    try:
                        # 安全地获取数据结构信息
                        if isinstance(data, dict):
                            # 检查API响应是否成功
                            if data.get('success', False) and 'result' in data and data['result'] is not None:
                                result = data['result']
                                if isinstance(result, dict) and 'data' in result:
                                    api_data = result['data']
                                    
                                    # 验证api_data的有效性
                                    if api_data is not None and isinstance(api_data, list) and len(api_data) > 0:
                                        logger.info(f"API返回 {len(api_data)} 条数据")
                                        try:
                                            sample_keys = list(api_data[0].keys())
                                            logger.debug(f"业绩预告样例字段: {sample_keys}")
                                            logger.debug(f"样例数据: {api_data[0]}")
                                        except Exception:
                                            pass
                                        
                                        # 转换为新闻格式
                                        news_list = []
                                        for item in api_data[:50]:  # 限制处理前50条数据
                                            if isinstance(item, dict):  # 确保item是字典类型
                                                forecast_news = self._convert_data_to_news(item)
                                                if forecast_news:
                                                    news_list.append(forecast_news)
                                        
                                        if news_list:
                                            logger.info(f"成功转换 {len(news_list)} 条业绩预告新闻")
                                            return news_list[:20]  # 限制返回20条数据
                                    else:
                                        logger.debug(f"API {api_url} 返回空数据或格式不正确")
                                else:
                                    logger.debug(f"API响应格式不正确: {list(result.keys()) if isinstance(result, dict) else type(result)}")
                            else:
                                logger.debug(f"API请求失败: {data.get('message', '未知错误')}")
                        else:
                            logger.debug(f"API返回数据类型不正确: {type(data)}")
                            
                    except Exception as e:
                        logger.debug(f"API {api_url} 数据解析失败: {str(e)}")
                        
            except Exception as e:
                logger.debug(f"API {api_url} 请求失败: {str(e)}")
 
            return []
 
        except Exception as e:
            logger.error(f"获取API数据失败: {str(e)}")
            return []
    
    def _request_json(self, url: str, params: Optional[Dict[str, Any]] = None, referer: Optional[str] = None, timeout: int = 15, max_retries: int = 3):
        """统一请求方法：
        - 设置Eastmoney常见请求头（User-Agent/Accept/Referer/Accept-Language）
        - 自动携带当前session cookies
        - 指数退避重试
        - 兼容JSONP（去函数名包裹）
        - 返回dict或list，否则返回None
        """
        headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
            'Accept': 'application/json, text/javascript, */*; q=0.01',
            'Accept-Language': 'zh-CN,zh;q=0.9',
            'Referer': referer or 'https://data.eastmoney.com/',
            'Connection': 'keep-alive',
        }
        delay = 0.6
        last_err = None
        for attempt in range(1, max_retries + 1):
            try:
                resp = self.session.get(url, params=params, headers=headers, timeout=timeout)
                status = resp.status_code
                if status != 200:
                    logger.debug(f"请求失败 status={status}, url={resp.url}")
                    raise RuntimeError(f"HTTP {status}")
                text = resp.text.strip()
                # JSONP剥壳：匹配 callback(...) 或 jQuery123(...)
                if (text.startswith('callback(') or text.startswith('jQuery') or text.endswith(')') and text.find('{') > -1):
                    l = text.find('(')
                    r = text.rfind(')')
                    if l != -1 and r != -1 and r > l:
                        text = text[l+1:r]
                try:
                    return json.loads(text)
                except Exception:
                    try:
                        return resp.json()
                    except Exception as je:
                        # 记录前1KB片段，便于排查
                        snippet = text[:1024]
                        logger.debug(f"JSON解析失败，片段: {snippet}")
                        last_err = je
                        raise
            except Exception as e:
                last_err = e
                if attempt < max_retries:
                    time.sleep(delay)
                    delay *= 1.6
                    continue
                break
        logger.debug(f"_request_json 重试耗尽: {str(last_err)}")
        return None
    

    
    def _generate_basic_forecast_news(self) -> List[Dict]:
        """生成基础业绩预告信息"""
        try:
            # 这里可以添加一些基础业绩预告数据的生成逻辑
            return []
        except Exception as e:
            logger.error(f"生成基础业绩预告新闻失败: {str(e)}")
            return []
    
    